package org.papervision3d.objects.parsers
{
	import flash.display.BitmapData;
	import flash.errors.IllegalOperationError;
	
	import org.ascollada.core.*;
	import org.ascollada.core.types.DaeColorOrTexture;
	import org.ascollada.events.*;
	import org.ascollada.fx.*;
	import org.papervision3d.Papervision3D;
	import org.papervision3d.core.animation.keyframe.AbstractKeyFrame;
	import org.papervision3d.core.animation.track.*;
	import org.papervision3d.core.controller.*;
	import org.papervision3d.core.data.UserData;
	import org.papervision3d.core.geom.TriangleMesh3D;
	import org.papervision3d.core.geom.renderables.Triangle3D;
	import org.papervision3d.core.geom.renderables.Vertex3D;
	import org.papervision3d.core.log.PaperLogger;
	import org.papervision3d.core.material.*;
	import org.papervision3d.core.math.Matrix3D;
	import org.papervision3d.core.math.NumberUV;
	import org.papervision3d.core.proto.MaterialObject3D;
	import org.papervision3d.core.render.data.RenderSessionData;
	import org.papervision3d.events.FileLoadEvent;
	import org.papervision3d.materials.*;
	import org.papervision3d.materials.shaders.ShadedMaterial;
	import org.papervision3d.materials.utils.MaterialsList;
	import org.papervision3d.objects.DisplayObject3D;
	import org.papervision3d.objects.special.Skin3D;

	/**
	 * 
	 */ 
	public class DAE extends DisplayObject3D
	{
		/**
		 * The COLLADA document. @see org.ascollada.DaeDocument
		 */ 
		public var document :DaeDocument;
		
		/**
		 * 
		 */ 
		public var controllers :Array;
		
		/**
		 * 
		 */ 
		public var animation :AnimationController;
		
		/**
		 * The animation tracks for this DAE, if any.
		 */ 
		private var _tracks :Array;
		
		/**
		 * 
		 */ 
		public function DAE(name:String=null)
		{
			super(name);
			
			
		}
		
		/**
		 * 
		 */ 
		public function getChildByDaeID(id:String, parent:DisplayObject3D=null):DisplayObject3D
		{
			parent = parent || this;
			
			var node :DaeNode = parent.userData ? parent.userData.data as DaeNode : null;
			
			if(node && node.id == id)
				return parent;
			
			for each(var child:DisplayObject3D in parent.children)
			{
				var object :DisplayObject3D = getChildByDaeID(id, child);
				if(object)
					return object;
			}
			
			return null;	
		}
		
		/**
		 * 
		 */ 
		public function getChildByDaeSID(sid:String, parent:DisplayObject3D=null):DisplayObject3D
		{
			parent = parent || this;
			
			var node :DaeNode = parent.userData ? parent.userData.data as DaeNode : null;

			if(node && node.sid == sid)
				return parent;
			
			for each(var child:DisplayObject3D in parent.children)
			{
				var object :DisplayObject3D = getChildByDaeSID(sid, child);
				if(object)
					return object;
			}
			
			return null;	
		}
		
		/**
		 * 
		 */ 
		public function load(asset:*, materials:MaterialsList=null):void
		{
			_tracks = new Array();
			
			this.materials = materials || new MaterialsList();
			this.controllers = new Array();
			this.document = new DaeDocument();
			this.document.addEventListener(DaeFileEvent.LOAD_COMPLETE, onColladaFileComplete);
			this.document.parse(asset);	
		}

		/**
		 * 
		 */
		public override function project(parent:DisplayObject3D, renderSessionData:RenderSessionData):Number
		{
			// update controllers
			for each(var controller:IObjectController in controllers)
			{
				controller.update();
			}
				
			return super.project(parent, renderSessionData);	
		}
		
		/**
		 * 
		 */ 
		protected function buildAnimation(object:DisplayObject3D, node:DaeNode):void
		{
			var matrixTrack : MatrixStackTrack = new MatrixStackTrack(object);
			var track : AbstractTrack;
			var transform : DaeTransform;
			var channel : DaeChannel;
			var i : int, j : int;
			
			for(i = 0; i < node.transforms.length; i++)
			{	
				transform = node.transforms[i];
				
				var channels : Array = new Array();
				for(j = 0; j < node.channels.length; j++)
				{
					channel = node.channels[j];
					if(channel.targetSID == transform.sid)
						channels.push(channel);
				}
				
				if(channels.length == 0)
				{
					track = buildAnimationTrack(object, transform);
					track.addKeyFrame(new AbstractKeyFrame(0, transform.data));
					
					if(track is RotationTrack)
					{
						var keyframe : AbstractKeyFrame = track.keyFrames[0];
						keyframe.data[3] *= (Math.PI/180);
					}
					matrixTrack.addTrack(track);
				}
				else
				{
					for(j = 0; j < channels.length; j++)
					{
						track = null;
						
						channel = channels[j];
						
						switch(transform.type)
						{
							case "matrix":
								track = buildMatrixTrack(object, transform, channel);
								break;
								
							case "rotate":
								track = buildRotationTrack(object, transform, channel);
								break;
								
							case "scale":
								track = buildScaleTrack(object, transform, channel);
								break;
								
							case "translate":
								track = buildTranslationTrack(object, transform, channel);
								break;
								
							default:
								break;
						}
						
						if(track)
						{
							matrixTrack.addTrack(track);
						}
					}
				}
			}

			_tracks.push(matrixTrack);
		}
		
		/**
		 * 
		 */ 
		protected function buildAnimations(object:DisplayObject3D, epsilon:Number=0.05):void
		{
			var document :DaeDocument = this.document;
			var node :DaeNode = object.userData ? object.userData.data as DaeNode : null;
			
			if(node && node.channels && node.channels.length)
			{
				buildAnimation(object, node);
			}
			
			for each(var child:DisplayObject3D in object.children)
			{
				buildAnimations(child);
			}
		}
		
		/**
		 * 
		 */ 
		protected function buildAnimationTrack(object:DisplayObject3D, transform:DaeTransform):AbstractTrack
		{
			switch(transform.type)
			{
				case "matrix":
					return new MatrixTrack(object);
				case "scale":
					return new ScaleTrack(object);
				case "translate":
					return new TranslationTrack(object);
				case "rotate":
					return new RotationTrack(object);
				default:
					break;
			}
			return null;
		}
		
		/**
		 * Builds a matrix track.
		 * 
		 * @param	target
		 * @param	transform
		 * @param	channel
		 * 
		 * @return The created MatrixTrack 
		 */
		protected function buildMatrixTrack(target:DisplayObject3D, transform:DaeTransform, channel:DaeChannel) : MatrixTrack
		{
			var track : MatrixTrack = new MatrixTrack(target);
			var input : Array = channel.sampler.input;
			var output : Array = channel.sampler.output;
			var i : int;
			
			for(i = 0; i < input.length; i++)
			{
				var time : Number = input[i];
				var data : Array = output[i] is Array ? output[i] : [output[i]];
				
				if(!channel.targetMember && channel.arrayIndex0 < 0 && channel.arrayIndex1 < 0 && data.length >= 12)
				{
					track.addKeyFrame(new AbstractKeyFrame(time, data));
				}
				else
				{
					trace("unhandled matrix channel : " + channel);
					return null;
				}
			}
			
			return track;
		}
		
		/**
		 * Builds a rotation track.
		 * 
		 * @param	target
		 * @param	transform
		 * @param	channel
		 * 
		 * @return The created RotationTrack 
		 */
		protected function buildRotationTrack(target:DisplayObject3D, transform:DaeTransform, channel:DaeChannel) : RotationTrack
		{
			var track : RotationTrack = new RotationTrack(target);
			var input : Array = channel.sampler.input;
			var output : Array = channel.sampler.output;
			var i : int;
			
			for(i = 0; i < input.length; i++)
			{
				var time : Number = input[i];
				var data : Array = [transform.data[0], transform.data[1], transform.data[2], 0];
				
				switch(channel.targetMember)
				{
					case "ANGLE":
						data[3] = output[i] * (Math.PI/180);
						break;
					default:
						trace("damn " + output[i]);
						continue;
				}

				track.addKeyFrame(new AbstractKeyFrame(time, data));
			}
			
			return track;
		}
		
		/**
		 * Builds a scale track.
		 * 
		 * @param	target
		 * @param	transform
		 * @param	channel
		 * 
		 * @return The created ScaleTrack 
		 */
		protected function buildScaleTrack(target:DisplayObject3D, transform:DaeTransform, channel:DaeChannel) : ScaleTrack
		{
			var track : ScaleTrack = new ScaleTrack(target);
			var input : Array = channel.sampler.input;
			var output : Array = channel.sampler.output;
			var i : int;
			
			for(i = 0; i < input.length; i++)
			{
				var time : Number = input[i];
				var data : Array = [1, 1, 1];
				
				switch(channel.targetMember)
				{
					case "X":
						data[0] = output[i];
						break;
					case "Y":
						data[1] = output[i];
						break;
					case "Z":
						data[2] = output[i];
						break;
					default:
						break;
				}
				
				track.addKeyFrame(new AbstractKeyFrame(time, data));
			}
			
			return track;
		}
		
		/**
		 * Builds a translation track.
		 * 
		 * @param	target
		 * @param	transform
		 * @param	channel
		 * 
		 * @return The created TranslationTrack 
		 */
		protected function buildTranslationTrack(target:DisplayObject3D, transform:DaeTransform, channel:DaeChannel) : TranslationTrack
		{
			var track : TranslationTrack = new TranslationTrack(target);
			var input : Array = channel.sampler.input;
			var output : Array = channel.sampler.output;
			var i : int;
			
			for(i = 0; i < input.length; i++)
			{
				var time : Number = input[i];
				var data : Array = [0, 0, 0];
				
				switch(channel.targetMember)
				{
					case "X":
						data[0] = output[i];
						break;
					case "Y":
						data[1] = output[i];
						break;
					case "Z":
						data[2] = output[i];
						break;
					default:
						break;
				}
				
				track.addKeyFrame(new AbstractKeyFrame(time, data));
			}
			
			return track;
		}
		
		/**
		 * 
		 */ 
		protected function buildGeometry(node:DaeNode, mesh:TriangleMesh3D):void
		{
			var i :int, j :int, k :int;
			var primitive :DaePrimitive;
			
			for(i = 0; i < node.geometries.length; i++)
			{
				var geom :DaeInstanceGeometry = node.geometries[i];
					
				var daeGeometry :DaeGeometry = this.document.geometries[geom.url];
				
				if(!daeGeometry)
				{
					PaperLogger.warning("Can't find DaeGeometry with id = '" + geom.url + "'");
					continue;
				}
				
				if(daeGeometry.mesh && daeGeometry.mesh.primitives)
				{
					var idx :int = mesh.geometry.vertices.length;
					
					mesh.geometry.vertices = mesh.geometry.vertices.concat(buildVertices(daeGeometry.mesh));

					for each(primitive in daeGeometry.mesh.primitives)
					{
						buildPrimitive(mesh, primitive, idx, geom);
					}
					
					// vendor extension of FCOLLADA (ColladaMax, ColladaMaya)
					// -> the source data of the verctices is animated
					// NOTE: this is *not* allowed by the COLLADA 1.4.1 spec
					if(daeGeometry.mesh.vertexChannels)
					{
						for(j = 0; j < daeGeometry.mesh.vertexChannels.length; j++)
						{
							var channel :DaeChannel = daeGeometry.mesh.vertexChannels[j];
							var index :int = Math.floor(channel.arrayIndex0 / 3);
							var member :int = channel.arrayIndex0 - (index * 3);
							var track :VertexTrack = new VertexTrack(mesh, index, member);
							
							for(k = 0; k < channel.sampler.input.length; k++)
							{
								track.addKeyFrame(new AbstractKeyFrame(channel.sampler.input[k], [channel.sampler.output[k]]));
							}
							
							_tracks.push(track);
						}
					}
				}
			}

			mesh.geometry.ready = true;
		}
		
		/**
		 * 
		 */ 
		protected function buildImagePath(filename:String, imagePath:String):String
		{
			var parts :Array = filename.split("/");
			
			parts.pop();
			
			return parts.join("/") + "/" + imagePath;
		}
		
		/**
		 * 
		 */ 
		protected function buildInstanceMaterial(instanceMaterial:DaeInstanceMaterial, which:String="diffuse"):Array
		{
			var material :MaterialObject3D;
			var bv :DaeBindVertexInput;
			var image :DaeImage;
			
			if(instanceMaterial)
			{
				var mat :DaeMaterial = this.document.materials[ instanceMaterial.target ];
				if(mat)
				{
					var effect :DaeEffect = this.document.effects[ mat.instance_effect ];
					var data :DaeColorOrTexture = effect.shader[ which ];
				
					if(data && data.texture && effect.surface)
					{
						bv = instanceMaterial.findBindVertexInput(data.texture.texcoord, "TEXCOORD");
					}
			
					material = this.materials.getMaterialByName(mat.name);
					
					if(material)
					{
						// material exists in the MaterialsList
					}
					else if(effect.surface || data.texture is DaeTexture)
					{
						image = effect.surface ? this.document.images[effect.surface.init_from] : this.document.images[data.texture.texture];

						if(image && image.data is BitmapData)
						{
							material = new BitmapMaterial(image.data);
							material.doubleSided = effect.double_sided;
							material.tiled = true;
						}
					}
					else if(data && data.color)
					{
						var r :int = data.color.r * 0xff;
						var g :int = data.color.g * 0xff;
						var b :int = data.color.b * 0xff;
						var color :uint = (r << 16 | g << 8 | b);
						
						material = new ColorMaterial(color, data.color.a);
						material.doubleSided = effect.double_sided;
					}
				}
			}
			return [material, bv];
		}
		
		/**
		 * 
		 */ 
		protected function buildPrimitive(mesh:TriangleMesh3D, primitive:DaePrimitive, idx:int, geom:DaeInstanceGeometry, which:String="diffuse"):void
		{		
			var instance_material :DaeInstanceMaterial = geom.bindMaterial ? geom.bindMaterial.findMaterialInstance(primitive.material) : null;
			var material :MaterialObject3D = MaterialObject3D.DEFAULT;
			var bv :DaeBindVertexInput;
			var image :DaeImage;
			var texture_set :int = 0;
			var inputSet :int = 0;
			var i :int;
			
			var materialInfo :Array = buildInstanceMaterial(instance_material);
			if(materialInfo.length == 2)
			{
				material = materialInfo[0];
				bv = materialInfo[1];
			}
material = new WireframeMaterial();

			if(bv)
			{
				inputSet = bv.input_set;
			}
			else
			{
				inputSet = primitive.getDefaultDataSetID("TEXCOORD");	
			}
			
			inputSet = inputSet < 0 ? 0 : inputSet;
			
			// shaded materials need registration
			if(material is AbstractLightShadeMaterial || material is AbstractSmoothShadeMaterial || material is ShadedMaterial)
			{
				material.registerObject(mesh);
			}
						
			// grab indices into the vertices data
			var vertexIndices :Array = primitive.getDataSet("VERTEX", 0);
			
			// grab indices into the uvt data from the correct set
			var uvtIndices :Array = primitive.getDataSet("TEXCOORD", inputSet);
			
			// grab the uvt data source from the correct set
			var uvtSourceID :String = primitive.getDataSourceID("TEXCOORD", inputSet);
			var uvtSource :DaeSource = document.sources[uvtSourceID];
			var uvtData :Array = uvtSource ? uvtSource.data : null;

			for(i = 0; i < vertexIndices.length; i++)
			{
				var v0 :Vertex3D = mesh.geometry.vertices[ vertexIndices[i][0] ];
				var v1 :Vertex3D = mesh.geometry.vertices[ vertexIndices[i][1] ];
				var v2 :Vertex3D = mesh.geometry.vertices[ vertexIndices[i][2] ];
				
				var uv0 :NumberUV;
				var uv1 :NumberUV;
				var uv2 :NumberUV;

				if(uvtIndices && uvtData)
				{
					var t0 :Array = uvtData[ uvtIndices[i][0] ];
					var t1 :Array = uvtData[ uvtIndices[i][1] ];
					var t2 :Array = uvtData[ uvtIndices[i][2] ];
					
					uv0 = Papervision3D.useRIGHTHANDED ? new NumberUV(1.0 - t0[0], t0[1]) : new NumberUV(t0[0], t0[1]);
					uv1 = Papervision3D.useRIGHTHANDED ? new NumberUV(1.0 - t1[0], t1[1]) : new NumberUV(t1[0], t1[1]);
					uv2 = Papervision3D.useRIGHTHANDED ? new NumberUV(1.0 - t2[0], t2[1]) : new NumberUV(t2[0], t2[1]);
				}
				else
				{
					uv0 = new NumberUV();
					uv1 = new NumberUV();
					uv2 = new NumberUV();
				}

				mesh.geometry.faces.push(new Triangle3D(mesh, [v0, v1, v2], material, [uv0, uv1, uv2]));
			}
		}
		
		/**
		 * 
		 */ 
		protected function buildNode(node:DaeNode, parent:DisplayObject3D):void
		{	
			var object :DisplayObject3D;
			var name :String = node.name;
			var skins :Array = node.getSkinControllers();
			var morphs :Array = node.getMorphControllers();
			
			if(parent.getChildByName(name))
				name += "_" + node.id;
				
			if(node.geometries.length)
			{
				if(skins.length)
				{
					object = new Skin3D(null, [], [], name);	
				}
				else if(morphs.length)
				{
					trace("MORPH");
				}
				else
				{
					object = new TriangleMesh3D(null, [], [], name);
				}
				buildGeometry(node, object as TriangleMesh3D);
			}
			else if(morphs.length)
			{
				var controller : DaeController = morphs.shift() as DaeController;
				var morph :DaeMorph = controller.morph;
				var instanceGeometry : DaeInstanceGeometry = new DaeInstanceGeometry(document);
				
				instanceGeometry.url = morph.source;
				node.geometries = [instanceGeometry];
				object = new TriangleMesh3D(null, [], [], name);
				buildGeometry(node, object as TriangleMesh3D);
				
				if(morph.weights.channels)
				{
					trace("morph weights are animated by #" + morph.weights.channels.length + " channels.");	
				}
				
				if(morphs.length)
				{
					PaperLogger.warning("node with id=" + node.id + " contains more then one morph controller! Only using first found.");
				}
			}
			else
			{
				object = new DisplayObject3D(name);
			}
			
			for(var i:int = 0; i < node.nodes.length; i++)
			{
				if(node.nodes[i] is DaeInstanceNode)
				{
					// instantiate a node	
					var instance_node :DaeInstanceNode = node.nodes[i];
					if(document.library_nodes[instance_node.url] is DaeNode)
					{
						buildNode(document.library_nodes[instance_node.url], object);
					}
					else
					{
						PaperLogger.warning("DAE#buildNode : node instance with id='" + instance_node.url + "' not found!");
					}
				}
				else if(node.nodes[i] is DaeNode)
				{
					buildNode(node.nodes[i], object);
				}	
			}

			parent.addChild(object);
			
			var transform :DaeTransform = node.getBakedTransform();
			
			object.copyTransform(new Matrix3D(transform.data));
			object.userData = new UserData(node);
		}
		
		/**
		 * Build the COLLADA scene.
		 */ 
		protected function buildScene():void
		{		
			var i:int;
			var tmp :Array = new Array();
			
			// loop over all <node> elements found in the COLLADA document	
			for(i = 0; i < this.document.nodes.length; i++)
			{
				buildNode(this.document.nodes[i], this);
			}
			
			for each(var child:DisplayObject3D in this.children)
			{
				buildAnimations(child);
			}
			
			if(_tracks.length)
			{
				this.animation = new AnimationController();
				for each(var track:AbstractTrack in _tracks)
				{
					this.animation.addTrack(track);
				}
				this.controllers.unshift(this.animation);	
			}
			
			linkSkins(this);
			
			// done!
			dispatchEvent(new FileLoadEvent(FileLoadEvent.LOAD_COMPLETE));
		}
		
		/**
		 * 
		 */ 
		protected function buildSkinController(object:Skin3D, skinController:DaeController):void
		{
			if(!skinController.skin)
				return;
				
			var skin :DaeSkin = skinController.skin;
			var controller :SkinController = new SkinController(object);
			var i :int;
			
			controller.joints = new Array();
			controller.vertexWeights = new Array();
			controller.invBindMatrices = new Array();
			controller.bindShapeMatrix = new Matrix3D(skin.bind_shape_matrix.values);
			
			for(i = 0; i < skin.joints.length; i++)
			{
				var jid :String = skin.joints[i];
				
				var joint :DisplayObject3D = this.getChildByDaeID(jid) || this.getChildByDaeSID(jid);

				if(!joint)
				{
					throw new IllegalOperationError("Could not find joint: " + jid);
				}
				
				var jidx :int = skin.getJointIndex(jid);
				var weights :Array = skin.getWeightsForJoint(jid);
				var bindMatrix :DaeMatrix = skin.inv_bind_matrix[jidx];
				
				controller.joints.push(joint);
				controller.vertexWeights.push(weights);
				controller.invBindMatrices.push(new Matrix3D(bindMatrix.values));
			}
			
			this.controllers.push(controller);
		}
		
		/**
		 * 
		 */ 
		protected function buildVertices(mesh:DaeMesh):Array
		{
			var vertices :Array = new Array();
			var data :Array;
			var i:int;
			
			for(i = 0; i < mesh.vertices.length; i++)
			{
				data = mesh.vertices[i];
				
				var v:Vertex3D = new Vertex3D(data[0], data[1], data[2]);
				
				vertices.push(v);	
			}
			
			return vertices;
		}
		
		/**
		 * 
		 */ 
		protected function linkSkins(object:DisplayObject3D):void
		{
			var node :DaeNode = object.userData ? object.userData.data as DaeNode : null;
			
			if(object is Skin3D && node)
			{
				var skins :Array = node.getSkinControllers();
				if(skins.length)
					buildSkinController(object as Skin3D, skins.shift() as DaeController);
			}
			
			for each(var child:DisplayObject3D in object.children)
			{
				linkSkins(child);
			}
		}
		
		/**
		 * 
		 */ 
		private function onColladaFileComplete(e:DaeFileEvent):void
		{
			buildScene();
		}
	}
}